﻿using System;
using System.Collections;
using System.Collections.Generic;
using System.Collections.ObjectModel;
using System.IO;
using System.Management.Automation;
using System.Management.Automation.Runspaces;
using System.Reflection;
using NUnit.Framework;
using System.Threading;

namespace NUnit4PowerShell
{
    [TestFixture]
    public class ExecutePowerShellFixture
    {
        /// <summary>
        /// PowerShell file path containing the PowerShell test function to execute
        /// </summary>
        private string _testFilePath = Utils.GetFilePath(FixtureProvider.GetOne());

        public ExecutePowerShellFixture()
        {
            Logger.Instance.Info("[ExecutePowerShellFixture] Fixture constructor", TestFileName);
            // If it's null it means that the DLL has been launched without using the console or any line
            // In that case only one fixture can be executed : the one specified on the configuration file
            // TODO: or the on first one in the folder (first or the only)
            if (string.IsNullOrEmpty(_testFilePath))
            {
                var files = Utils.GetPSFixtureFiles();
                Logger.Instance.Info("[ExecutePowerShellFixture][Constructor] TestFilePath : files[0]", TestFileName);
                _testFilePath = files[0];
                
            }
            InitializeFixture();
            Logger.Instance.AddEmptyLine();
        }

        #region Private Members
        /// <summary>
        /// Common runspace for the PowerShell commands execution
        /// </summary>
        private Runspace _powershellRunspace;

        private const string SetUpFixtureFunctionName = "SetUp-TestFixture";
        private  const string SetUpFunctionName = "SetUp-Test";
        private const string TearDownFunctionName = "TearDown-Test";
        private const string TearDownFixtureFunctionName = "TearDown-TestFixture";

        /// <summary>
        /// Mock file overriding functions not handled by the PowerShell runspace instantiated
        /// </summary>
        private readonly string _mockScriptPath = Utils.GetMockCmdLetScriptPath();

        /// <summary>
        /// Path of the folder containing the test PowerShell files
        /// </summary>
        private readonly string _folderPath = Utils.GetFolderPath();
        private readonly string _testFunctionNamePattern = Utils.GetPSTestFunctionNamePattern();

        private bool _setUpFixtureFunctionExists;
        private bool _setUpFunctionExists;
        private bool _tearDownFunctionExists; 
        private bool _tearDownFixtureFunctionExists;

        /// <summary>
        /// This list contains all the PowerShell functions to test inside this fixture and executed by the TestCaseSource
        /// </summary>
        private static IList<string> _functionsToTestList;
        #endregion

        #region Properties
        /// <summary>
        /// Provide the function list to test inside this fixture
        /// </summary>
        /// <returns></returns>
        public IEnumerable<string> FunctionsToTest()
        {
            if (_functionsToTestList == null)
            {
                throw new NullReferenceException("Functions list is null");
            }
            return _functionsToTestList;
        }

        private string TestFileName
        {
            get
            {
                return Path.GetFileNameWithoutExtension(_testFilePath);
            }
        }
        
        #endregion

        private void InitializeFixture()
        {
            Uri u = new Uri(Assembly.GetExecutingAssembly().CodeBase);
            string dir = System.IO.Path.GetDirectoryName(u.LocalPath);
            System.Environment.CurrentDirectory = dir;
            
            Logger.Instance.Info("[InitializeFixture] Current directory is : " + dir, TestFileName );

            using (Runspace runspace = PowerShellRunspaceSetup())
            {
                try
                {
                    // Get PowerShell functions used for TestFixtureSetUp, SetUp, TearDown and TestFixtureTearDown
                    _setUpFixtureFunctionExists = FindPowerShellFixtureMethod(SetUpFixtureFunctionName, runspace);
                    _setUpFunctionExists = FindPowerShellFixtureMethod(SetUpFunctionName, runspace);
                    _tearDownFunctionExists = FindPowerShellFixtureMethod(TearDownFunctionName, runspace);
                    _tearDownFixtureFunctionExists = FindPowerShellFixtureMethod(TearDownFixtureFunctionName, runspace);

                    // Get all functions to test in this fixture
                    _functionsToTestList = FindPowerShellTestsToRun(_testFunctionNamePattern, runspace);
                    Logger.Instance.Info("[InitializeFixture] Count of function to test : " + _functionsToTestList.Count,TestFileName);
                    foreach (string functionName in _functionsToTestList)
                    {
                        Logger.Instance.Info("[InitializeFixture] Functions to test" + functionName, TestFileName);
                    }

                }
                catch (Exception exception)
                {
                    Logger.Instance.Error("[InitializeFixture]" + exception, TestFileName);
                    throw;
                }
                finally
                {
                    runspace.Close();
                }
            }
        }


        #region PowerShell Methods
        /// <summary>
        /// Prepare the PowerShell runspace for tests
        /// </summary>
        /// <returns>PowerShell runspace prepared</returns>
        private Runspace PowerShellRunspaceSetup()
        {
            try
            {
                Runspace runspace = RunspaceFactory.CreateRunspace();
                runspace.ThreadOptions = PSThreadOptions.UseCurrentThread;
                runspace.Open();

                // We set the location to the current Scripts path locaton
                ExecuteCmdlet(string.Format("Set-Location -Path '{0}'", _folderPath), runspace);
                if (_mockScriptPath != null)
                {   
                    // We have to mock some functions as Write-Host
                    ExecuteCmdlet(string.Format(@"Import-Module '{0}' -force -DisableNameChecking", _mockScriptPath), runspace);
                }
                // We import functions to test here
                ExecuteCmdlet(string.Format("Import-Module '{0}' -force -DisableNameChecking", _testFilePath), runspace);
                
                return runspace;
            }
            catch (Exception exception)
            {
                Logger.Instance.Error("[PowerShellRunspaceSetup]" + exception, TestFileName);
                throw;
            }
        }

        /// <summary>
        /// This method functions following a name pattern
        /// </summary>
        /// <param name="pattern">Pattern used to find test functions</param>
        /// <returns>List of all function name following the pattern</returns>
        private static List<string> FindPowerShellTestsToRun(string pattern, Runspace runspace)
        {

            // List of the functions to run
            List<string> functionsToTest = new List<string>();

            // Get all Powershell functions containing the test name pattern defined
            Collection<PSObject> resultPSObject = ExecuteCmdlet(string.Format("Get-Item function:{0}", pattern), runspace);

            // And add them in a list
            foreach (PSObject psObject in resultPSObject)
            {
                functionsToTest.Add(psObject.ToString());
            }

            return functionsToTest;
        }

        private bool FindPowerShellFixtureMethod(string functionName, Runspace runspace)
        {
            Collection<PSObject> resultPSObject;
            try
            {
                resultPSObject = ExecuteCmdlet(string.Format("Get-Item function:{0}", functionName), runspace);
            }
            catch (ItemNotFoundException)
            {
                //Log.Logger.Debug(string.Format("[FindPowerShellFixtureMethod] Function '{0}' has not been found", functionName));
                return false;
            }
            if (resultPSObject.Count == 1)
            {
                //Log.Logger.Debug(string.Format("[FindPowerShellFixtureMethod] Find '{0}' function ", functionName));
                return true;
            }
            else
            {
                throw new Exception(string.Format("Several functions {0} have been found in {1}", functionName, _testFilePath));
            }
        }

        /// <summary>
        /// This method executes a PowerShell command in a runspace and return the result.
        /// </summary>
        /// <param name="command"></param>
        /// <param name="powershellRunspace"></param>
        /// <returns></returns>
        private static Collection<PSObject> ExecuteCmdlet(string command, Runspace powershellRunspace)
        {
            try
            {
                using (Pipeline pipeline = powershellRunspace.CreatePipeline(command))
                {
                    try
                    {
                        //Log.Logger.Debug(string.Format("[ExecuteCmdlet] Execution of '{0}'", command));
                        Collection<PSObject> resultObject = pipeline.Invoke();
                        var errors = pipeline.Error.ReadToEnd();
                        if (errors.Count > 0)
                        {
                            string error =
                                ((System.Management.Automation.ErrorRecord)
                                 (((System.Management.Automation.PSObject) (errors[0])).ImmediateBaseObject)).
                                    FullyQualifiedErrorId;

                            var exception =
                                ((System.Management.Automation.ErrorRecord)
                                 (((System.Management.Automation.PSObject) (errors[0])).BaseObject)).Exception;

                            throw exception;
                            //TODO : Rename this exception with a dedicated exception
                            //TODO : Cast exception to not raise NUnit exception
                        }
                        return resultObject;
                    }
                    catch (Exception exception)
                    {
                        if (exception.InnerException != null &&
                            exception.InnerException.ToString().ToLower().Contains("nunit.framework"))
                        {
                            throw exception.InnerException;
                        }
                        throw;
                    }
                }
            }
            catch (System.Management.Automation.RuntimeException exception)
            {
                // Get the real reason of the exception : if not we do not get the cmdlet name responsible of the exception
                InvocationInfo errorInvocationInfo = exception.ErrorRecord.InvocationInfo;
                if (errorInvocationInfo == null)
                {
                    throw;
                }
                string error = CreateErrorMessage(errorInvocationInfo);
                throw new System.Management.Automation.CmdletProviderInvocationException(error, exception);
            }
            catch (System.Management.Automation.PSInvalidCastException exception)
            {
                // Get the real reason of the exception : if not we do not get the cmdlet name responsible of the exception
                InvocationInfo errorInvocationInfo = exception.ErrorRecord.InvocationInfo;
                string error = CreateErrorMessage(errorInvocationInfo);
                throw new System.Management.Automation.CmdletProviderInvocationException(error, exception);
            }
            catch (NUnit.Framework.AssertionException exception)
            {
                throw;
            }
            catch (NUnit.Framework.IgnoreException exception)
            {
                throw;
            }
            catch (NUnit.Framework.SuccessException exception)
            {
                throw;
            }
            catch (NUnit.Framework.InconclusiveException exception)
            {
                throw;
            }
        }

        /// <summary>
        /// Methods which format a detailled message error 
        /// </summary>
        /// <param name="errorInvocationInfo"></param>
        /// <returns></returns>
        private static string CreateErrorMessage(InvocationInfo errorInvocationInfo)
        {
            string errorMessage = "";
            string errorLine = errorInvocationInfo.Line.Trim();
            int errorLineNumber = errorInvocationInfo.ScriptLineNumber;
            string scriptFullPath = errorInvocationInfo.ScriptName.Trim();
            string scriptName = Path.GetFileName(errorInvocationInfo.ScriptName);
            Dictionary<string, object> parameters = errorInvocationInfo.BoundParameters;

            // Get values of arguments
            string parameterString = "";
            foreach (KeyValuePair<string, object> parameter in parameters)
            {
                if (parameter.Value != null)
                {
                    string value = "";
                    if (parameter.Value is string[])
                    {
                        foreach (string aValue in (string[])parameter.Value)
                        {
                            value += string.Format("\"{0}\"", aValue);
                        }
                    }
                    else
                    {
                        value = string.Format("\"{0}\"", parameter.Value.ToString());
                    }
                    parameterString += string.Format("-{0} {1} ", parameter.Key, value);
                }
                else
                {
                    parameterString += string.Format("-{0} ", parameter.Key);
                }
            }

            // Build the message
            errorMessage = string.Format("{0}An error occured at line {1} in {2}", Environment.NewLine, errorLineNumber, scriptName);
            errorMessage += string.Format("{0} Command line generating an error is :", Environment.NewLine);
            errorMessage += string.Format("{0}  {1}", Environment.NewLine, errorLine);
            errorMessage += string.Format("{0} Command line is :", Environment.NewLine);
            errorMessage += string.Format("{0}  {1} {2}", Environment.NewLine, errorInvocationInfo.InvocationName, parameterString);
            errorMessage += string.Format("{0} In :", Environment.NewLine);
            errorMessage += string.Format("{0}  {1}", Environment.NewLine, scriptFullPath);

            return errorMessage;
        }
        #endregion

        #region Fixture methods
        [TestFixtureSetUp]
        public void TestFixtureSetup()
        {
            Logger.Instance.Info("TestFixtureSetup", TestFileName);
            using (_powershellRunspace = PowerShellRunspaceSetup())
            {
                if (_setUpFixtureFunctionExists)
                {
                    Logger.Instance.Info("[TestFixtureSetup] PowerShell method execution", TestFileName);
                    ExecuteCmdlet(SetUpFixtureFunctionName, _powershellRunspace);
                }
                _powershellRunspace.Close();
            }
        }

        [SetUp]
        public void SetUp()
        {
            Logger.Instance.Info("SetUp", TestFileName);
            _powershellRunspace = PowerShellRunspaceSetup();
            if (_setUpFunctionExists)
            {
                Logger.Instance.Info("[Setup] PowerShell method execution", TestFileName);
                ExecuteCmdlet(SetUpFunctionName, _powershellRunspace);
            }
        }

        [Test]
        [Sequential]
        [TestCaseSource("FunctionsToTest")]
        public void PowerShell_Tests(string functionToTest)
        {
            Logger.Instance.Info("[Test] PowerShell method execution of " + functionToTest, TestFileName);
            ExecuteCmdlet(functionToTest, _powershellRunspace);
        }

        [TearDown]
        public void TearDown()
        {
            try
            {
                Logger.Instance.Info("TearDown", TestFileName);
                if (_tearDownFunctionExists)
                {
                    Logger.Instance.Info("[TearDown] PowerShell method execution", TestFileName);
                    ExecuteCmdlet(TearDownFunctionName, _powershellRunspace);
                }

            }
            finally
            {
                _powershellRunspace.Close();
                _powershellRunspace.Dispose();
                _powershellRunspace = null;
            }
        }

        [TestFixtureTearDown]
        public void TestFixtureTearDown()
        {
            Logger.Instance.Info("TestFixtureTearDown", TestFileName);
            using (_powershellRunspace = PowerShellRunspaceSetup())
            {
                if (_tearDownFixtureFunctionExists)
                {
                    Logger.Instance.Info("[TestFixtureTearDown] PowerShell method execution", TestFileName);
                    ExecuteCmdlet(TearDownFixtureFunctionName, _powershellRunspace);
                }

                _powershellRunspace.Close();
            }
            Logger.Instance.AddEmptyLine();
        }
        #endregion
    }
}
    